#!/usr/bin/php
<?php
//               _    _
//    __   _____| | _| |_ ___  _ __ _ __ __ _ _   _ _ __ ___
//    \ \ / / _ \ |/ / __/ _ \| '__| '__/ _` | | | | '_ ` _ \
//     \ V /  __/   <| || (_) | |  | | | (_| | |_| | | | | | |
//      \_/ \___|_|\_\\__\___/|_|  |_|  \__,_|\__,_|_| |_| |_|
//
//    checkpassword-oath
//
//    Checkpassword compatible application for verifying users based on
//    OATH HOTP and TOTP Tokens (RFC 4226).
//
//    Also supporting device specific passwords so the users password doesn't
//    have to be stored on each device, in case the device is compromised or
//    lost the device password can easily be revoked.
//
//    It is also within the scope of this project to develop a simple user front
//    end which can be used to authorize tokens and generate device passwords.
//
//    Author: Fabian Valka <office@infosec.at>
//    Project Page: http://code.google.com/p/checkpassword-oath/
//
//    License:
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
/******************************************************************************
 * Configuration Options
 ******************************************************************************/
$config = array(); // Don't edit

/*
 * Database
 */
$config['db_type'] = 'mysql'; // Database type e.g. mysql, pgsql, sqlite
$config['db_host'] = 'localhost'; // Hostname
$config['db_database'] = 'mailserver'; // Name of the database
$config['db_user'] = 'root'; // Database access username
$config['db_pass'] = '';

/*
 * Extended Passwords
 */
$config['extended_pass_length'] = 12; // Length of the random device password
$config['extended_pass_algo'] = 'whirlpool';

/*
 * HOTP and TOTP Configuration
 */
$config['hotp_length'] = 6; // Length of the HOTP and TOTP generated key
$config['hotp_time_intervall'] = 60; // How long is the time interval for TOTP keys
$config['hotp_time_window_before'] = -30; // Maximum time in past for key
$config['hotp_time_window_future'] = 30; // Maximum time window into the future
$config['hotp_counter_steps'] = 50; // Maximum counter steps to compute

/*
 * Database queries
 */
// Query for selecting the user based on username
$config['query_user'] = "select virtual_users.id AS id, concat(virtual_users.user,'@',virtual_domains.name) AS username,virtual_users.password AS password from (virtual_users left join virtual_domains on((virtual_users.domain_id = virtual_domains.id))) WHERE concat(virtual_users.user,'@',virtual_domains.name)=:username";
$config['query_extended_pass'] = 'SELECT id, type, password, salt FROM virtual_user_passwords WHERE user_id=:user_id';
$config['query_hotp_settings'] = 'SELECT id, password, counter, last FROM virtual_user_hotp WHERE password_id=:password_id';
$config['query_increment_counter'] = 'UPDATE virtual_user_hotp SET counter = :counter, last = :last WHERE id=:id';
$config['query_insert_extended_pass'] = 'INSERT INTO virtual_user_passwords (user_id, password, salt) VALUES (:user_id, :password, :salt);';

/*
 * Return codes
 */
$config['return_valid'] = 0;
$config['return_invalid'] = 1;
$config['return_error'] = 111;

/*
 * Extended Password Types
 */
$config['type_normal'] = 0;
$config['type_hotp_counter'] = 1;
$config['type_hotp_time'] = 2;

/*
 * Log settings
 */
$config['log_file'] = 'c:/checkpassword.log';
// Use bitwise OR to combine log levels, e.g. INFO | WARN
$config['log_level'] = CheckPasswordLogger::ALL;

/******************************************************************************
 * DONT EDIT BELOW THIS LINE UNLESS YOU REALLY KNOW WHAT YOU ARE DOING
 ******************************************************************************/

$checkPass = new CheckPassword($config);

// Authorize the user
if(!isset ($dont_authorize))
    $checkPass->authorize($argv);

/**
 * Checkpassword class
 *
 * Does the password verification
 *
 * @author Fabian Valka
 * @copyright 2011
 */
 class CheckPassword {
     private $db;
     private $config;
     private $logger;

     // Constructor initializing database
     public function __construct($config) {
         // Connect to the database using the settings
         $this->db = new PDO("$config[db_type]:host=$config[db_host];dbname=$config[db_database]",
             $config['db_user'], $config['db_pass']);

         // Copy settings into config var
         $this->config = $config;

         $this->logger = new CheckPasswordLogger($config['log_file'], $config['log_level']);
         
     }


     public function authorize($argv) {
         $username = "";
         $password = "";


         $this->logger->log_debug('Authorizing user, reading from filestream (cat fd3)');

         // get user data from input stream
         $this->readFromFd($username, $password);

         $return_code = $this->verify($username, $password);

         if($return_code==$this->config['return_valid']) {
             if(isset ($argv[1]))
                shell_exec($argv[1]);
         }

         exit($return_code);
     }

     /**
      * Reads the password and username from file descriptor 3
      *
      * Format on file descriptor 3: username\0password\0
      *
      */
     protected function readFromFd(&$username, &$password) {
        // Use cat because the file descriptor can not be directyl read
        $fp = popen('/bin/cat /proc/self/fd/3','r');


         $this->logger->log_debug('File stream opened (cat fd3)');

        // how many nulls have been found
        $nullCount = 0;

        // read the input char by char
        while(false !== ($char = fgetc($fp))) {
            if($char=="\0") {
                    // if we found two chars we are done
                    if(++$nullCount==2) {

                             $this->logger->log_debug('Read complete');
                            return;
                    }
            } else {
                    // append to username and password
                    if($nullCount == 0) {
                            $username .= $char;
                    } else {
                            $password .= $char;
                    }
            }

        }

        //fclose($fp);
        pclose($fp);

     }


     public function verify($username, $password) {
         $this->logger->log_debug('Verifying user ' . $username);
         $userResult = $this->fetchUser($username);

         // Check if result set is either 0 or to large
         if(sizeof($userResult)>1) {
            $this->logger->log_error('Non unique username ' . $username);
             return $this->config['return_error'];
         } elseif (sizeof($userResult)==0) {

             $this->logger->log_debug('No user found ' . $username);
             $this->logger->log_info('Invalid login ' . $username . ' (notfound)');
             return $this->config['return_invalid'];
         }

         // Check if the login is just using the normal password
         if($userResult[0]['password']==md5($password)) {

             $this->logger->log_debug('User verified using default user password');

             $this->logger->log_info('User ' . $username . ' authorized. (default password)');
             return $this->config['return_valid'];
         }

         // Continue with checking against both HOTP and TOTP as well as
         // device password database

         $extended_password = '';
         $hotp = null;

         // If it starts with as many letters as an HOTP or TOTP Token
         // would containt check if it is a combination of a valid token and
         // password
         if(preg_match("/^[0-9]{" . $this->config['hotp_length'] . "}.*/", $password) > 0) {
             $this->logger->log_debug('possible HOTP password');
             $hotp = substr($password, 0, $this->config['hotp_length']);
             $extended_password = substr($password, $this->config['hotp_length']);
         } else {
             $extended_password = $password;
         }

         // Password id write back var
         $password_id = 0;

         // Verify the extended password against the database
         $this->logger->log_debug('trying to identify the extended password user ' . $username . ' id ' . $userResult[0]['id']);
         $password_verify_result = $this->verifyExtendedPassword($userResult[0]['id'], $extended_password,$password_id);

         // Check what the result says
         if($password_verify_result==-1) {
             // Password not valid as entered
             // Check if the password is valid without using substring for
             // making numerical extended passwords
             $this->logger->log_debug('trying to identify user ' . $username . ' with extended password which was assumed to be HOTP');

             if(!is_null($hotp) &&
                 $this->verifyExtendedPassword($userResult[0]['id'], $password,$password_id)==$this->config['type_normal']) {
                 // Valid all numerical password
                 $this->logger->log_info('User ' . $username . ' authorized. (extended password)');
                 
                 return $this->config['return_valid'];
             }

             $this->logger->log_info('Invalid login ' . $username . ' (wrong password)');
             // Invalid password
             return $this->config['return_invalid'];
         } elseif ($password_verify_result==$this->config['type_normal']) {
             // Normal (non HOTP Password)
             return $this->config['return_valid'];
             $this->logger->log_info('User ' . $username . ' authorized. (extended password)');
         } elseif ($password_verify_result==$this->config['type_hotp_counter'] ||
                    $password_verify_result==$this->config['type_hotp_time']) {
             $this->logger->log_debug('trying HOTP verification for user ' . $username . ' token ' . $hotp);
             // HOTP Password! Verifying using HOTP class
             if($this->verfiyHotp($password_id,$hotp,$password_verify_result)) {
                 $this->logger->log_info('User ' . $username . ' authorized. (HOTP)');
                 
                 return $this->config['return_valid'];
             }
             
         } else {
             $this->logger->log_error('User ' . $username . ' has an invalid password type in the database.');
             // Invalid type
             return $this->config['return_error'];
         }

         $this->logger->log_info('Invalid login ' . $username . ' (wrong password)');
         // No password matched return error
         return $this->config['return_invalid'];

         
     }

     /**
      * Verifies a HOTP Token both HOTP and TOTP
      *
      * @param <int> $password_id Id of the password.
      * @param <string> $hotp The value of the token.
      * @param <int> $type The token type see configuration.
      * @return <bool> true if correct, false otherwise
      */
     protected function verfiyHotp($password_id,$hotp,$type) {
         $this->logger->log_debug('verifyHOTP(' . $password_id . ',' . $hotp . ',' . $type . ')');

         $query = $this->db->prepare($this->config['query_hotp_settings']);
         
         // Bind the parameter, 256 = maximum length of emailadress
         $query->bindParam(':password_id',$password_id,PDO::PARAM_INT);

         $this->logger->log_debug('Executing query_hotp_settings to database');
         $query->execute();
         $result = $query->fetchAll();

         // Allow multiple tokens using the same password
         foreach($result as $row) {
             // Normal HOTP Counter Token
             if($type==$this->config['type_hotp_counter']) {

                 // Allow future counters up to the configured value
                 for($i=1; $i<=$this->config['hotp_counter_steps']; $i++) {

                    // Compute counter for this iteration from database data
                    $hotp_from_db = HOTP::generateByCounter($this->hexstr($row['password']), $row['counter'] + $i)->toHOTP($this->config['hotp_length']);

                    // Too much output
                    //$this->logger->log_debug(sprintf("HOTP Counter: Input: %s Db: %s Counter: %s",$hotp,$hotp_from_db,$row['counter'] + $i));



                    // Valid password
                    if($hotp_from_db==$hotp) {
                        // Check for reuse, should not happen due to counter increase
                        if($hotp==$row['last']) {
                            $this->logger->log_debug('reuse detected'); 
                            return false;
                        }

                        $this->logger->log_debug('valid HOTP password found, counter');

                        // Increment the counter
                        $queryIncrement = $this->db->prepare($this->config['query_increment_counter']);

                        $queryIncrement->bindValue(':counter',$row['counter'] + $i);
                        $queryIncrement->bindParam(':last', $hotp_from_db);
                        $queryIncrement->bindParam(':id', $row['id']);

                        $this->logger->log_debug('executing query_increment_counter');
                        $queryIncrement->execute();

                        // Valid token
                        return true;
                    }
                 }
             } elseif ($type==$this->config['type_hotp_time']) {
                 $this->logger->log_debug('verifying TOTP password');
                 // NOT IMPLEMENTED YET
                 return false;
             }
         }

         $this->logger->log_debug('No HOTP Password found');
         // No matching password found
         return false;
     }

     /**
      * Verifies an extended password against the database
      *
      * @param <type> $userid User id
      * @param <type> $password Password
      * @param <type> $password_id Write back variable for the password id
      * @return <int> Type of the password, see configuration.
      */
     protected function verifyExtendedPassword($userid,$password,&$password_id) {
         $this->logger->log_debug('verifying extended password userid ' . $userid);
         $query = $this->db->prepare($this->config['query_extended_pass']);

         // Bind the parameter, 256 = maximum length of emailadress
         $query->bindParam(':user_id',$userid,PDO::PARAM_INT);

         $this->logger->log_debug('executing query query_extended_pass');
         $query->execute();
         $result = $query->fetchAll();

         foreach($result as $row) {
             if(hash($this->config['extended_pass_algo'], $password . $row['salt'])==$row['password']) {
                 // Write password id into output var
                 $password_id = $row['id'];

                 return $row['type'];
             }
         }

         // No password matched
         return -1;
     }

     public function addExtended($userid,$password) {
         $this->logger->log_debug('adding extended password');
         $query = $this->db->prepare($this->config['query_insert_extended_pass']);

         $salt = $this->createRandomPassword(32);

         $password_hash = hash($this->config['extended_pass_algo'], $password . $salt);

         // Bind the parameter, 256 = maximum length of emailadress
         $query->bindParam(':user_id',$userid,PDO::PARAM_INT);
         $query->bindParam(':salt', $salt,PDO::PARAM_STR);
         $query->bindParam(':password', $password_hash,PDO::PARAM_STR);

         $this->logger->log_debug('executing query query_insert_extended_pass');

         $query->execute();

         $this->logger->log_info('Added extended password for user ' . $user);
     }

     public function fetchUser($username) {
         $this->logger->log_debug('fetching user ' . $username . ' from database');
         $query = $this->db->prepare($this->config['query_user']);

         // Bind the parameter, 256 = maximum length of emailadress
         $query->bindParam(':username',$username,PDO::PARAM_STR,256);

         $this->logger->log_debug('executing query_user');
         $query->execute();
         $result = $query->fetchAll();

         return $result;
     }

     /**
      * Generates a random string
      *
      * @return <type>
      */
     public function createRandomPassword($length, $hex = false) {

        $chars = '';
        
        if($hex) {
            $chars = "abcdef0123456789";
        } else {
            $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        }

        srand();
        $i = 0;
        $pass = '' ;

        while ($i <= $length) {
            $num = mt_rand() % 33;
            $tmp = substr($chars, $num, 1);
            $pass = $pass . $tmp;
            $i++;
        }

        return $pass;

    }

    public function hexstr($hex) {
        $str = '';

        for($i=0; $i < strlen($hex); $i+=2) {
            $str .= chr(hexdec(substr($hex, $i, 2)));
        }

        return $str;
    }
}


 class CheckPasswordLogger {
     // Define log levels
     const ERROR = 1;
     const WARN = 2;
     const INFO = 4;
     const DEBUG = 8;
     
     const ALL = 255;

     // current log level
     private $loglevel;
     private $logfh;

     public function __construct($logfile, $loglevel) {
         $this->logfh = fopen($logfile, 'a');
         $this->loglevel = $loglevel;
     }

     public function __destruct() {
         // close the log file on destruct
         fclose($this->logfh);
     }

     /**
      * Writes an entry into the log.
      *
      * @param <type> $text Text to write, excluding time
      * @param <type> $loglevel Default INFO
      */
     public function log($text, $loglevel = self::INFO) {
         // Check if logging the current input is enabled
        if(($loglevel & $this->loglevel) > 0) {
            fwrite($this->logfh, '[' . date(DATE_RFC822) . '] ' . escapeshellcmd($text) . "\n");
        }
     }

     // Functions for easier logging
     public function log_debug($text) {
         $this->log('DEBUG: ' . $text, self::DEBUG);
     }

     public function log_info($text) {
         $this->log($text,self::INFO);
     }

     public function log_warn($text) {
         $this->log($text, self::WARN);
     }

     public function log_error($test) {
         $this->log($text,self::ERROR);
     }
 }

/**
 * HOTP Class
 * Based on the work of OAuth, and the sample implementation of HMAC OTP
 * http://tools.ietf.org/html/draft-mraihi-oath-hmac-otp-04#appendix-D
 * @author Jakob Heuser (firstname)@felocity.com
 * @copyright 2011
 * @version 1.0
 */
class HOTP {
    /**
     * Generate a HOTP key based on a counter value (event based HOTP)
     * @param string $key the key to use for hashing
     * @param int $counter the number of attempts represented in this hashing
     * @return HOTPResult a HOTP Result which can be truncated or output
     */
    public static function generateByCounter($key, $counter) {
        // the counter value can be more than one byte long,
        // so we need to pack it down properly.
        $cur_counter = array(0, 0, 0, 0, 0, 0, 0, 0);
        for($i = 7; $i >= 0; $i--) {
            $cur_counter[$i] = pack ('C*', $counter);
            $counter = $counter >> 8;
        }

        $bin_counter = implode($cur_counter);

        // Pad to 8 chars
        if (strlen($bin_counter) < 8) {
            $bin_counter = str_repeat (chr(0), 8 - strlen ($bin_counter)) . $bin_counter;
        }

        // HMAC
        $hash = hash_hmac('sha1', $bin_counter, $key);

        return new HOTPResult($hash);
    }

    /**
     * Generate a HOTP key based on a timestamp and window size
     * @param string $key the key to use for hashing
     * @param int $window the size of the window a key is valid for in seconds
     * @param int $timestamp a timestamp to calculate for, defaults to time()
     * @return HOTPResult a HOTP Result which can be truncated or output
     */
    public static function generateByTime($key, $window, $timestamp = false) {
        if (!$timestamp && $timestamp !== 0) {
            $timestamp = HOTP::getTime();
        }

        $counter = intval($timestamp / $window);

        return HOTP::generateByCounter($key, $counter);
    }

    /**
     * Generate a HOTP key collection based on a timestamp and window size
     * all keys that could exist between a start and end time will be included
     * in the returned array
     * @param string $key the key to use for hashing
     * @param int $window the size of the window a key is valid for in seconds
     * @param int $min the minimum window to accept before $timestamp
     * @param int $max the maximum window to accept after $timestamp
     * @param int $timestamp a timestamp to calculate for, defaults to time()
     * @return HOTPResult a HOTP Result which can be truncated or output
     */
    public static function generateByTimeWindow($key, $window, $min, $max, $timestamp = false) {
        if (!$timestamp && $timestamp !== 0) {
            $timestamp = HOTP::getTime();
        }

        $counter = intval($timestamp / $window);
        $window = range($min, $max);

        for ($i = 0; $i < count($window); $i++) {
            $shift_counter = $window[$i];
            $out[$shift_counter] = HOTP::generateByCounter($key, $counter + $shift_counter);
        }

        return $out;
    }

    /**
     * Gets the current time
     * Ensures we are operating in UTC for the entire framework
     * Restores the timezone on exit.
     * @return int the current time
     */
    public static function getTime() {
        $tz = date_default_timezone_get();
        date_default_timezone_set("UTC");
        $timestamp = time();
        date_default_timezone_set($tz);

        return $timestamp;
    }
}

/**
 * The HOTPResult Class converts an HOTP item to various forms
 * Supported formats include hex, decimal, string, and HOTP
 * @author Jakob Heuser (firstname)@felocity.com
 */
class HOTPResult {
    protected $hash;
    protected $binary;
    protected $decimal;

    /**
     * Build an HOTP Result
     * @param string $value the value to construct with
     */
    public function __construct($value) {
        // store raw
        $this->hash = $value;

        // store calculate decimal
        $hmac_result = array();

        // Convert to decimal
        foreach(str_split($this->hash,2) as $hex)
        {
            $hmac_result[] = hexdec($hex);
        }

        $offset = $hmac_result[19] & 0xf;

        $this->decimal = (
            (($hmac_result[$offset+0] & 0x7f) << 24 ) |
            (($hmac_result[$offset+1] & 0xff) << 16 ) |
            (($hmac_result[$offset+2] & 0xff) << 8 ) |
            ($hmac_result[$offset+3] & 0xff)
        );

        // calculate hex
        $this->hex = dechex($this->decimal);
    }

    /**
     * Returns the string version of the HOTP
     * @return string
     */
    public function toString() {
        return $this->hash;
    }

    /**
     * Returns the hex version of the HOTP
     * @return string
     */
    public function toHex() {
        return $this->hex;
    }

    /**
     * Returns the decimal version of the HOTP
     * @return int
     */
    public function toDec() {
        return $this->decimal;
    }

    /**
     * Returns the truncated decimal form of the HOTP
     * @param int $length the length of the HOTP to return
     * @return string
     */
    public function toHOTP($length) {
        $str = str_pad($this->toDec(), $length, "0", STR_PAD_LEFT);
        $str = substr($str, (-1 * $length));
        return $str;
    }

}

?>
